1 2 /* 3 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 4 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 5 Authors: Marcelo S. N. Mancini 6 7 Copyright Marcelo S. N. Mancini 2018 - 2021. 8 Distributed under the CC BY-4.0 License. 9 (See accompanying file LICENSE.txt or copy at 10 https://creativecommons.org/licenses/by/4.0/ 11 */ 12 module hip.game2d.sprite; 13 public import hip.api.renderer.texture; 14 public import hip.api.graphics.color; 15 public import hip.api.data.commons; 16 import hip.math.vector; 17 import hip.api.renderer.shaders.spritebatch; 18 import hip.api.data.textureatlas; 19 import hip.assets.texture; 20 21 /** 22 * Encapsulates bunch of sprites to hold a contiguous list of vertices. 23 * It has some advantages than creating manually an array of similar sprites such as: 24 * - Copies only once to the SpriteBatch, which searches for the texture index once. 25 * - Boundary checks once 26 * - Makes the sprites array vertices linear, reducing cache misses. 27 * - Wraps the setTexture and draw process so no need to manually execute the foreach 28 */ 29 class HipMultiSprite 30 { 31 protected HipSpriteVertex[] vertices; 32 HipSprite[] sprites; 33 IHipTexture texture; 34 this(size_t spritesCount) 35 { 36 vertices = new HipSpriteVertex[4*spritesCount]; 37 sprites = new HipSprite[spritesCount]; 38 foreach(i; 0..spritesCount) 39 sprites[i] = new HipSprite(vertices[i*4..(i+1)*4]); 40 } 41 42 ref HipSprite opIndex(size_t index){return sprites[index];} 43 44 int opApply(scope int delegate(ref HipSprite) dg) 45 { 46 int result = 0; 47 foreach (item; sprites) 48 { 49 result = dg(item); 50 if (result) 51 break; 52 } 53 return result; 54 } 55 56 57 int opApply(scope int delegate(size_t index, ref HipSprite) dg) 58 { 59 int result = 0; 60 foreach (i, item; sprites) 61 { 62 result = dg(i, item); 63 if (result) 64 break; 65 } 66 return result; 67 } 68 69 70 void setTexture(IHipTexture texture) 71 { 72 this.texture = texture; 73 foreach(sp; sprites) 74 sp.setTexture(texture); 75 } 76 77 ref HipSpriteVertex[] getVertices() 78 { 79 //Vertices is already a data sink for the sprites, so no need to reassign. 80 foreach(i, sp; sprites) 81 sp.getVertices; 82 return vertices; 83 } 84 85 void draw() 86 { 87 import hip.api.graphics.g2d.renderer2d; 88 foreach(sp; sprites) 89 sp.isDirty = true; 90 drawSprite(texture, cast(ubyte[])getVertices()); 91 } 92 } 93 94 class HipSprite 95 { 96 IHipTextureRegion texture; 97 HipColor color = HipColor.white; 98 float x = 0, y = 0; 99 float scrollX = 0, scrollY = 0; 100 float rotation = 0; 101 //Tiling == 1 for consistency, 0 the image would disappear 102 float tilingX = 1, tilingY = 1; 103 float scaleX = 1, scaleY = 1; 104 105 ///Origin is a point that defines that offsets the rendering origin X and Y based on the sprite size 106 float originX = 0.5, originY = 0.5; 107 108 float u1 = 0, v1 = 0, u2 = 0, v2 = 0; 109 110 ///Width of the texture region, (u2-u1) * texture.width 111 uint width; 112 ///Height of the texture region, (v2-v1) * texture.height 113 uint height; 114 115 private bool flippedX, flippedY; 116 117 protected bool isDirty = true; 118 protected HipSpriteVertex[] vertices; 119 120 package this(HipSpriteVertex[] sink) 121 { 122 this.vertices = sink; 123 setColor(HipColor.white); 124 } 125 126 this() 127 { 128 import hip.api; 129 vertices = new HipSpriteVertex[4]; 130 setColor(HipColor.white); 131 setTexture(cast()HipDefaultAssets.getDefaultTexture()); 132 } 133 this(IHipAssetLoadTask task) 134 { 135 this(); 136 setTexture(task); 137 } 138 139 this(IHipTexture texture) 140 { 141 vertices = new HipSpriteVertex[4]; 142 setTexture(texture); 143 } 144 this(IHipTextureRegion region) 145 { 146 vertices = new HipSpriteVertex[4]; 147 this.texture = region; 148 width = region.getWidth(); 149 height = region.getHeight(); 150 setRegion(region.getRegion()); 151 } 152 153 void setTexture(IHipTexture texture) 154 { 155 import hip.api; 156 this.texture = new HipTextureRegion(texture); 157 width = texture.getWidth; 158 height = texture.getHeight; 159 setRegion(this.texture.getRegion()); 160 } 161 void setTexture(IHipAssetLoadTask task) 162 { 163 import hip.api; 164 HipAssetManager.addOnCompleteHandler(task, (asset) 165 { 166 this.setTexture(cast(IHipTexture)asset); 167 }); 168 } 169 170 final IHipTexture getTexture() { return texture.getTexture();} 171 172 final void setRegion(float u1, float v1, float u2, float v2) 173 { 174 setRegion(TextureCoordinatesQuad(u1,v1,u2,v2)); 175 } 176 void setRegion(IHipTextureRegion region) 177 { 178 width = region.getWidth(); 179 height = region.getHeight(); 180 texture = region; 181 setRegion(region.getRegion()); 182 } 183 void setRegion(TextureCoordinatesQuad c) 184 { 185 this.u1 = c.u1; 186 this.u2 = c.u2; 187 this.v1 = c.v1; 188 this.v2 = c.v2; 189 190 texture.setRegion(c.u1, c.v1, c.u2, c.v2); 191 width = texture.getWidth; 192 height = texture.getHeight; 193 const float[] v = texture.getVertices(); 194 195 vertices[0].vTexST = Vector2(v[0], v[1]); 196 vertices[1].vTexST = Vector2(v[2], v[3]); 197 vertices[2].vTexST = Vector2(v[4], v[5]); 198 vertices[3].vTexST = Vector2(v[6], v[7]); 199 if(flippedX) 200 { 201 flippedX = false; 202 setFlippedX(true); 203 } 204 if(flippedY) 205 { 206 flippedY = false; 207 setFlippedY(true); 208 } 209 } 210 211 void setPosition(float x, float y) 212 { 213 if(this.x != x || this.y != y) 214 isDirty = true; 215 this.x = x; 216 this.y = y; 217 } 218 void setOrigin(float x, float y) 219 { 220 if(originX != x || originY != y) 221 isDirty = true; 222 originX = x; 223 originY = y; 224 } 225 226 ref HipSpriteVertex[] getVertices() 227 { 228 if(isDirty) 229 { 230 isDirty = false; 231 float _x = -cast(float)width*originX * scaleX + x; 232 float _y = -cast(float)height*originY * scaleY + y; 233 float x2 = _x+(width * scaleX); 234 float y2 = _y+(height * scaleY); 235 236 if(rotation == 0) 237 { 238 //Top left 239 vertices[0].vPosition = Vector3(_x, _y,0); 240 241 //Top right 242 vertices[1].vPosition = Vector3(x2, _y,0); 243 244 //Bot right 245 vertices[2].vPosition = Vector3(x2, y2,0); 246 247 //Bot left 248 vertices[3].vPosition = Vector3(_x, y2,0); 249 } 250 else 251 { 252 import core.math:sin,cos; 253 float c = cos(rotation); 254 float s = sin(rotation); 255 256 //Top left 257 vertices[0].vPosition = Vector3(c*_x - s*_y + this.x, c*_y + s*_x + this.y,0); 258 259 //Top right 260 vertices[1].vPosition = Vector3(c*x2 - s*_y + this.x, c*_y + s*x2 + this.y,0); 261 262 //Bot right 263 vertices[2].vPosition = Vector3(c*x2 - s*y2 + this.x, c*y2 + s*x2 + this.y,0); 264 265 //Bot left 266 vertices[3].vPosition = Vector3(c*_x - s*y2 + this.x, c*y2 + s*_x + this.y,0); 267 } 268 } 269 return vertices; 270 } 271 272 void setColor(HipColor color) 273 { 274 this.color = color; 275 vertices[0].vColor = color; 276 vertices[1].vColor = color; 277 vertices[2].vColor = color; 278 vertices[3].vColor = color; 279 } 280 281 void setScale(float scaleX, float scaleY) 282 { 283 this.scaleX = scaleX; 284 this.scaleY = scaleY; 285 isDirty = true; 286 } 287 void setRotation(float rotation) 288 { 289 import hip.math.utils; 290 this.rotation = rotation % (PI * 2); 291 isDirty = true; 292 } 293 ///Same thing as setRotation, but receives in Degrees 294 void setAngle(float angle) 295 { 296 import hip.math.utils:degToRad; 297 setRotation(degToRad(angle)); 298 } 299 300 int getWidth() const {return width;} 301 int getHeight() const {return height;} 302 int getTextureWidth() const {return texture.getTextureWidth();} 303 int getTextureHeight() const {return texture.getTextureHeight();} 304 305 /** 306 * This function is most useful for single images. For instance backgrounds, probably, if you have a 307 * texture atlas or a spritesheet, this function is not useful 308 */ 309 void setScroll(float x, float y) 310 { 311 setRegion( 312 -scrollX + x + u1, 313 -scrollY + y + v1, 314 -scrollX + x + u2, 315 -scrollY + y + v2 316 ); 317 scrollX = x; 318 scrollY = y; 319 } 320 321 void setFlippedX(bool flip) 322 { 323 if(flip != flippedX) 324 { 325 auto reg = texture.getRegion; 326 flippedX = flip; 327 vertices[0].vTexST.x = flip ? reg.u2 : reg.u1; 328 vertices[1].vTexST.x = flip ? reg.u1 : reg.u2; 329 vertices[2].vTexST.x = flip ? reg.u1 : reg.u2; 330 vertices[3].vTexST.x = flip ? reg.u2 : reg.u1; 331 } 332 } 333 void setFlippedY(bool flip) 334 { 335 if(flip != flippedY) 336 { 337 auto reg = texture.getRegion; 338 flippedY = flip; 339 vertices[0].vTexST.y = flip ? reg.v2 : reg.v1; 340 vertices[1].vTexST.y = flip ? reg.v2 : reg.v1; 341 vertices[2].vTexST.y = flip ? reg.v1 : reg.v2; 342 vertices[3].vTexST.y = flip ? reg.v1 : reg.v2; 343 } 344 } 345 bool isFlippedX() => flippedX; 346 bool isFlippedY() => flippedY; 347 348 349 /** 350 * Sets the tiling factor for this sprite. Default is 1. 351 */ 352 void setTiling(float x = 1, float y = 1) 353 { 354 assert(x != 0 && y != 0, "Tiling factor equals 0 will disappear the sprite image"); 355 356 setRegion( 357 u1 / tilingX * x, 358 v1 / tilingY * y, 359 u2 / tilingX * x, 360 v2 / tilingY * y 361 ); 362 tilingX = x; 363 tilingY = y; 364 } 365 366 void draw() 367 { 368 import hip.api.graphics.g2d.renderer2d; 369 this.isDirty = true; 370 drawSprite(texture.getTexture, cast(ubyte[])getVertices[]); 371 } 372 } 373 374 375 class HipSpriteAnimation : HipSprite 376 { 377 import hip.api.graphics.g2d.animation; 378 private IHipAnimation animation; 379 HipAnimationFrame* currentFrame; 380 381 this(){super();} 382 383 this(IHipAnimation anim) 384 { 385 super(); 386 animation = anim; 387 this.setAnimation(anim.getCurrentTrackName()); 388 } 389 390 IHipAnimationTrack getAnimation(string animName) 391 { 392 return animation.getTrack(animName); 393 } 394 /** 395 * Sets internal animation data. 396 */ 397 void setAnimation(IHipAnimation anim) 398 { 399 animation = anim; 400 setAnimation(animation.getCurrentTrackName()); 401 } 402 void setAnimation(string animName) 403 { 404 animation.play(animName); 405 setFrame(animation.getCurrentFrame()); 406 } 407 408 void setBounds(int width, int height) 409 { 410 this.width = width; 411 this.height = height; 412 } 413 414 void setFrame(HipAnimationFrame* frame) 415 { 416 this.currentFrame = frame; 417 this.texture = frame.region; 418 setBounds(frame.region.getWidth(), frame.region.getHeight()); 419 setRegion(texture.getRegion()); 420 } 421 422 void update(float dt) 423 { 424 animation.update(dt); 425 setFrame(animation.getCurrentFrame()); 426 } 427 }